/*
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 */
package net.sf.l2j.gameserver.datatables;

import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import javolution.util.FastList;
import javolution.util.FastMap;
import net.sf.l2j.Config;
import net.sf.l2j.gameserver.model.L2EnchantSkillLearn;
import net.sf.l2j.gameserver.model.L2PledgeSkillLearn;
import net.sf.l2j.gameserver.model.L2Skill;
import net.sf.l2j.gameserver.model.L2SkillLearn;
import net.sf.l2j.gameserver.model.actor.instance.L2PcInstance;
import net.sf.l2j.gameserver.model.base.ClassId;
import net.sf.l2j.gameserver.xmlfactory.XMLDocumentFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

public class SkillTreeTable
{
	private static Logger _log = Logger.getLogger(SkillTreeTable.class.getName());

	private Map<ClassId, Map<Integer, L2SkillLearn>> _skillTrees;
    private List<L2SkillLearn> _fishingSkillTrees; // common skills (teached by Fisherman)
    private List<L2SkillLearn> _expandDwarfCraftSkillTrees; // special skill for dwarf (expand dwarf craft) learned by class teacher
    private List<L2PledgeSkillLearn> _pledgeSkillTrees; // pledge skill list
    private List<L2EnchantSkillLearn> _enchantSkillTrees; // enchant skill list

	public static SkillTreeTable getInstance()
	{
		return SingletonHolder._instance;
	}
	
	private SkillTreeTable()
	{
		load();
	}
	
	/**
	 * Return the minimum level needed to have this Expertise.<BR><BR>
	 * @param grade The grade level searched
	 */
	public int getExpertiseLevel(int grade)
	{
		if (grade <= 0)
			return 0;

		// since expertise comes at same level for all classes we use paladin for now
		Map<Integer, L2SkillLearn> learnMap = getSkillTrees().get(ClassId.paladin);

		int skillHashCode = SkillTable.getSkillHashCode(239, grade);
        if (learnMap.containsKey(skillHashCode))
            return learnMap.get(skillHashCode).getMinLevel();

		_log.severe("Expertise not found for grade " + grade);
		return 0;
	}

	/**
     * Each class receives new skill on certain levels, this methods allow the retrieval of the minimun character level
     * of given class required to learn a given skill
     * @param skillId The iD of the skill
     * @param classID The classId of the character
     * @param skillLvl The SkillLvl
     * @return The min level
     */
    public int getMinSkillLevel(int skillId, ClassId classId, int skillLvl)
    {
        Map<Integer, L2SkillLearn> map = getSkillTrees().get(classId);

        int skillHashCode = SkillTable.getSkillHashCode(skillId, skillLvl);
        if (map.containsKey(skillHashCode))
            return map.get(skillHashCode).getMinLevel();

        return 0;
    }

    public int getMinSkillLevel(int skillId, int skillLvl)
    {
    	int skillHashCode = SkillTable.getSkillHashCode(skillId, skillLvl);

    	// Look on all classes for this skill (takes the first one found)
    	for (Map<Integer, L2SkillLearn> map : getSkillTrees().values())
    	{
    		// checks if the current class has this skill
            if (map.containsKey(skillHashCode))
                return map.get(skillHashCode).getMinLevel();
    	}
        return 0;
    }

    private void load()
	{
        // Fishing skills tree && Expand dwarf craft skills tree
		try
		{
            _fishingSkillTrees = new FastList<L2SkillLearn>();
            _expandDwarfCraftSkillTrees = new FastList<L2SkillLearn>();
            
			File f = new File(Config.DATAPACK_ROOT + "/data/xml/skillstrees/fishing_skills_tree.xml");
			Document doc = XMLDocumentFactory.getInstance().loadDocument(f);
			
			Node n = doc.getFirstChild();
			for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling())
			{
				if ("skill".equalsIgnoreCase(d.getNodeName()))
				{
					NamedNodeMap attrs = d.getAttributes();
					int skillId = Integer.parseInt(attrs.getNamedItem("id").getNodeValue());
					int skillLvl = Integer.parseInt(attrs.getNamedItem("lvl").getNodeValue());
					int minLvl = Integer.parseInt(attrs.getNamedItem("minLvl").getNodeValue());
					int itemId = Integer.parseInt(attrs.getNamedItem("itemId").getNodeValue());
					int count = Integer.parseInt(attrs.getNamedItem("count").getNodeValue());
					boolean isDwarf = Boolean.parseBoolean(attrs.getNamedItem("isDwarf").getNodeValue());
					
					L2SkillLearn skill = new L2SkillLearn(skillId, skillLvl, minLvl, 0, itemId, count);
					
					if (isDwarf)
						_expandDwarfCraftSkillTrees.add(skill);
					else
						_fishingSkillTrees.add(skill);
				}
			}
		}
		catch (Exception e)
		{
			_log.warning("FishingTable: Error while loading fishing skills: " + e);
		}

		// Enchant skills tree
        try
        {
            _enchantSkillTrees = new FastList<L2EnchantSkillLearn>();

			File f = new File(Config.DATAPACK_ROOT + "/data/xml/skillstrees/enchant_skills_tree.xml");
			Document doc = XMLDocumentFactory.getInstance().loadDocument(f);
			
			Node n = doc.getFirstChild();
			for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling())
			{
				if ("enchant".equalsIgnoreCase(d.getNodeName()))
				{
					int minSkillLvl = 0;
					int id = Integer.valueOf(d.getAttributes().getNamedItem("id").getNodeValue());
					String name = String.valueOf(d.getAttributes().getNamedItem("name").getNodeValue());
					int baseLvl = Integer.valueOf(d.getAttributes().getNamedItem("base_lvl").getNodeValue());
					
					for (Node t = d.getFirstChild(); t != null; t = t.getNextSibling())
					{
						if ("data".equalsIgnoreCase(t.getNodeName()))
						{
							NamedNodeMap attrs = t.getAttributes();
							
							int lvl = Integer.valueOf(attrs.getNamedItem("level").getNodeValue());
							int sp = Integer.valueOf(attrs.getNamedItem("sp").getNodeValue());
							int exp = Integer.valueOf(attrs.getNamedItem("exp").getNodeValue());
							byte rate76 = Byte.valueOf(attrs.getNamedItem("rate_76").getNodeValue());
							byte rate77 = Byte.valueOf(attrs.getNamedItem("rate_77").getNodeValue());
							byte rate78 = Byte.valueOf(attrs.getNamedItem("rate_78").getNodeValue());

							if (lvl == 101 || lvl == 141)
								minSkillLvl = baseLvl;
							else
								minSkillLvl = lvl - 1;

							L2EnchantSkillLearn skill = new L2EnchantSkillLearn(id, lvl, minSkillLvl, baseLvl, name, sp, exp, rate76, rate77, rate78);
							_enchantSkillTrees.add(skill);
						}
					}
				}
			}
        }
        catch (Exception e)
        {
            _log.severe("EnchantSkillTable: Error while loading enchant skills tree: " + e);
        }
        
        // Pledge skills tree
        try
        {
            _pledgeSkillTrees = new FastList<L2PledgeSkillLearn>();
            
			File f = new File(Config.DATAPACK_ROOT + "/data/xml/skillstrees/pledge_skills_tree.xml");
			Document doc = XMLDocumentFactory.getInstance().loadDocument(f);
			
			Node n = doc.getFirstChild();
			for (Node nb = n.getFirstChild(); nb != null; nb = nb.getNextSibling())
			{
				if ("clan".equals(nb.getNodeName()))
				{
					final NamedNodeMap nbAttr = nb.getAttributes();
					final int clanLvl = Integer.parseInt(nbAttr.getNamedItem("lvl").getNodeValue());
					
					for (Node nc = nb.getFirstChild(); nc != null; nc = nc.getNextSibling())
					{
						if ("skill".equals(nc.getNodeName()))
						{
							final NamedNodeMap attr = nc.getAttributes();
							final int skillId = Integer.parseInt(attr.getNamedItem("id").getNodeValue());
							final int skillLvl = Integer.parseInt(attr.getNamedItem("lvl").getNodeValue());
							final int repCost = Integer.parseInt(attr.getNamedItem("repCost").getNodeValue());
							final int itemId = Integer.parseInt(attr.getNamedItem("itemId").getNodeValue());
							
							L2PledgeSkillLearn skill = new L2PledgeSkillLearn(skillId, skillLvl, clanLvl, repCost, itemId);
							_pledgeSkillTrees.add(skill);
						}
					}
				}
			}
        }
        catch (Exception e)
        {
            _log.severe("PledgeTable: Error while loading pledge skills: " + e);
        }

        _log.config("FishingSkillTreeTable: Loaded " + _fishingSkillTrees.size() + " general skills.");
        _log.config("DwarvenCraftSkillTreeTable: Loaded " + _expandDwarfCraftSkillTrees.size() + " dwarven skills.");
        _log.config("EnchantSkillTreeTable: Loaded " + _enchantSkillTrees.size() + " enchant skills.");
        _log.config("PledgeSkillTreeTable: Loaded " + _pledgeSkillTrees.size() + " pledge skills.");
    }

    public Map<ClassId, Map<Integer, L2SkillLearn>> getSkillTrees()
    {
        if (_skillTrees == null)
            _skillTrees = new FastMap<ClassId, Map<Integer, L2SkillLearn>>();

        return _skillTrees;
    }

	public void addSkillsToSkillTrees(final List<L2SkillLearn> skills, final int classId, final int parentId)
	{
		if (skills == null || skills.isEmpty())
			return;
		
		Map<Integer, L2SkillLearn> tmp = new FastMap<Integer, L2SkillLearn>();
		
		if (parentId > -1)
		{
			Map<Integer, L2SkillLearn> parent = getSkillTrees().get(ClassId.values()[parentId]);
			
			if (parent != null)
				for (L2SkillLearn skillLearn : parent.values())
					if (skillLearn != null)
						tmp.put(SkillTable.getSkillHashCode(skillLearn.getId(), skillLearn.getLevel()), skillLearn);
		}
		
		for (L2SkillLearn skillLearn : skills)
			if (skillLearn != null)
				tmp.put(SkillTable.getSkillHashCode(skillLearn.getId(), skillLearn.getLevel()), skillLearn);
		
		if (!tmp.isEmpty())
			getSkillTrees().put(ClassId.values()[classId], tmp);
	}
    
	public L2SkillLearn[] getAvailableSkills(L2PcInstance cha, ClassId classId)
	{
		List<L2SkillLearn> result = new FastList<L2SkillLearn>();
		Collection<L2SkillLearn> skills = getSkillTrees().get(classId).values();

		if (skills == null)
		{
			// the skilltree for this class is undefined, so we give an empty list
			_log.warning("Skilltree for class " + classId + " is not defined !");
			return new L2SkillLearn[0];
		}

		L2Skill[] oldSkills = cha.getAllSkills();

        for (L2SkillLearn temp : skills)
        {
        	// Exception for Lucky skill ; it can't be learnt back once lost.
        	if (temp.getId() == L2Skill.SKILL_LUCKY)
        		continue;
        	
            if (temp.getMinLevel() <= cha.getLevel())
            {
                boolean knownSkill = false;

                for (int j = 0; j < oldSkills.length && !knownSkill; j++)
                {
					L2Skill skill = oldSkills[j];
					if (skill != null && skill.getId() == temp.getId())
					{
						knownSkill = true;
						
						// this is the next level of a skill that we know
						if (skill.getLevel() == temp.getLevel() - 1)
							result.add(temp);
					}
                }
                
                // this is a new skill
                if (!knownSkill && temp.getLevel() == 1)
                    result.add(temp);
            }
        }

        return result.toArray(new L2SkillLearn[result.size()]);
	}

	public L2SkillLearn[] getAvailableSkills(L2PcInstance cha)
	{
	    List<L2SkillLearn> result = new FastList<L2SkillLearn>();
	    List<L2SkillLearn> skills = new FastList<L2SkillLearn>();

        skills.addAll(_fishingSkillTrees);

        if (cha.hasDwarvenCraft() && _expandDwarfCraftSkillTrees != null)
            skills.addAll(_expandDwarfCraftSkillTrees);

        L2Skill[] oldSkills = cha.getAllSkills();

        for (L2SkillLearn temp : skills)
        {
            if (temp.getMinLevel() <= cha.getLevel())
            {
                boolean knownSkill = false;

                for (int j = 0; j < oldSkills.length && !knownSkill; j++)
                {
                    if (oldSkills[j].getId() == temp.getId())
                    {
                        knownSkill = true;
                        
                        // this is the next level of a skill that we know
                        if (oldSkills[j].getLevel() == temp.getLevel() - 1)
                            result.add(temp);
                    }
                }
                
                // this is a new skill
                if (!knownSkill && temp.getLevel() == 1)
                    result.add(temp);
            }
        }

        return result.toArray(new L2SkillLearn[result.size()]);
	}

    public L2EnchantSkillLearn[] getAvailableEnchantSkills(L2PcInstance cha)
    {
        List<L2EnchantSkillLearn> result = new FastList<L2EnchantSkillLearn>();
        List<L2EnchantSkillLearn> skills = new FastList<L2EnchantSkillLearn>();

        skills.addAll(_enchantSkillTrees);

        L2Skill[] oldSkills = cha.getAllSkills();

        for (L2EnchantSkillLearn temp : skills)
        {
            if (76 <= cha.getLevel())
            {
                boolean knownSkill = false;

                for (int j = 0; j < oldSkills.length && !knownSkill; j++)
                {
                    if (oldSkills[j].getId() == temp.getId() )
                    {
                        knownSkill = true;
                        
                        // this is the next level of a skill that we know
                        if ( oldSkills[j].getLevel() == temp.getMinSkillLevel())
                            result.add(temp);
                    }
                }

            }
        }
        return result.toArray(new L2EnchantSkillLearn[result.size()]);
    }

    public L2PledgeSkillLearn[] getAvailablePledgeSkills(L2PcInstance cha)
    {
        List<L2PledgeSkillLearn> result = new FastList<L2PledgeSkillLearn>();
        List<L2PledgeSkillLearn> skills = _pledgeSkillTrees;

        if (skills == null)
        {
            // the skilltree for this class is undefined, so we give an empty list

            _log.warning("No clan skills defined!");
            return new L2PledgeSkillLearn[0];
        }

        L2Skill[] oldSkills = cha.getClan().getAllSkills();

        for (L2PledgeSkillLearn temp : skills)
        {
            if (temp.getBaseLevel() <= cha.getClan().getLevel())
            {
                boolean knownSkill = false;

                for (int j = 0; j < oldSkills.length && !knownSkill; j++)
                {
                    if (oldSkills[j].getId() == temp.getId() )
                    {
                        knownSkill = true;
                        
                        // this is the next level of a skill that we know
                        if ( oldSkills[j].getLevel() == temp.getLevel()-1)
                            result.add(temp);
                    }
                }

                // this is a new skill
                if (!knownSkill && temp.getLevel() == 1)
                    result.add(temp);
            }
        }
        return result.toArray(new L2PledgeSkillLearn[result.size()]);
    }
    
	/**
	 * Returns all allowed skills for a given class.
	 * @param classId
	 * @return all allowed skills for a given class.
	 */
	public Collection<L2SkillLearn> getAllowedSkills(ClassId classId)
	{
		return getSkillTrees().get(classId).values();
	}

	public int getMinLevelForNewSkill(L2PcInstance cha, ClassId classId)
	{
        int minLevel = 0;
		Collection<L2SkillLearn> skills = getSkillTrees().get(classId).values();

		if (skills == null)
		{
			// the skilltree for this class is undefined, so we give an empty list
			_log.warning("Skilltree for class " + classId + " is not defined !");
			return minLevel;
        }

        for (L2SkillLearn temp : skills)
        {
            if (temp.getMinLevel() > cha.getLevel() && temp.getSpCost() != 0)
                if (minLevel == 0 || temp.getMinLevel() < minLevel)
                    minLevel = temp.getMinLevel();
		}

		return minLevel;
	}

    public int getMinLevelForNewSkill(L2PcInstance cha)
    {
        int minLevel = 0;
        List<L2SkillLearn> skills = new FastList<L2SkillLearn>();

        skills.addAll(_fishingSkillTrees);

        if (cha.hasDwarvenCraft() && _expandDwarfCraftSkillTrees != null)
            skills.addAll(_expandDwarfCraftSkillTrees);

        for (L2SkillLearn s : skills)
        {
            if (s.getMinLevel() > cha.getLevel())
                if (minLevel == 0 || s.getMinLevel() < minLevel)
                    minLevel = s.getMinLevel();
        }
        return minLevel;
    }

    public int getSkillCost(L2PcInstance player, L2Skill skill)
    {
        int skillCost = 100000000;
        ClassId classId = player.getSkillLearningClassId();
        int skillHashCode = SkillTable.getSkillHashCode(skill);

        if (getSkillTrees().get(classId).containsKey(skillHashCode))
        {
        	L2SkillLearn skillLearn = getSkillTrees().get(classId).get(skillHashCode);
        	if (skillLearn.getMinLevel() <= player.getLevel())
            {
                skillCost = skillLearn.getSpCost();
                if (!player.getClassId().equalsOrChildOf(classId))
                	return skillCost;
            }
        }
        return skillCost;
    }

    public int getSkillSpCost(L2PcInstance player, L2Skill skill)
    {
        int skillCost = 100000000;
        L2EnchantSkillLearn[] enchantSkillLearnList = getAvailableEnchantSkills(player);

        for (L2EnchantSkillLearn enchantSkillLearn : enchantSkillLearnList)
        {
            if (enchantSkillLearn.getId() != skill.getId())
                continue;

            if (enchantSkillLearn.getLevel() != skill.getLevel())
                continue;

            if (76 > player.getLevel())
                continue;

            skillCost = enchantSkillLearn.getSpCost();
        }
        return skillCost;
    }

    public int getSkillExpCost(L2PcInstance player, L2Skill skill)
    {
        int skillCost = 100000000;
        L2EnchantSkillLearn[] enchantSkillLearnList = getAvailableEnchantSkills(player);

        for (L2EnchantSkillLearn enchantSkillLearn : enchantSkillLearnList)
        {
            if (enchantSkillLearn.getId() != skill.getId())
                continue;

            if (enchantSkillLearn.getLevel() != skill.getLevel())
                continue;

            if (76 > player.getLevel())
                continue;

            skillCost = enchantSkillLearn.getExp();
        }
        return skillCost;
    }

    public byte getSkillRate(L2PcInstance player, L2Skill skill)
    {
        L2EnchantSkillLearn[] enchantSkillLearnList = getAvailableEnchantSkills(player);

        for (L2EnchantSkillLearn enchantSkillLearn : enchantSkillLearnList)
        {
            if (enchantSkillLearn.getId() != skill.getId())
                continue;

            if (enchantSkillLearn.getLevel() != skill.getLevel())
                continue;

            return enchantSkillLearn.getRate(player);
        }
        return 0;
    }
    
	@SuppressWarnings("synthetic-access")
	private static class SingletonHolder
	{
		protected static final SkillTreeTable _instance = new SkillTreeTable();
	}
}